package com.harry.decoupling.impl.tool;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

import com.amazonaws.ClientConfiguration;
import com.amazonaws.SdkBaseException;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
import com.amazonaws.services.s3.model.ListObjectsRequest;
import com.amazonaws.services.s3.model.ObjectListing;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.S3ObjectSummary;
import com.harry.decoupling.defi.proxy.GenericSrvProxy;
import com.harry.decoupling.defi.tool.BinaryDataManager;
import com.harry.resource.inner.entity.InnerFile;
import com.harry.utility.BeanCtxUtils;
import com.harry.utility.EnvUtils;
import com.harry.utility.InnerUtils;
import com.harry.utility.query.InnerCriteria;
import com.harry.utility.query.InnerCriterion;
import com.harry.utility.query.InnerProjection;

@Component
public class AwsBinaryDataManagerImpl implements BinaryDataManager {

	@Override
	public Object src(String location) throws Exception {
		return s3task(s3 -> {
			if (ifRemoteFileExists(location)) {
				GeneratePresignedUrlRequest urlRequest = new GeneratePresignedUrlRequest(buName, workingFolder(location));
				urlRequest.setExpiration(new Date(new Date().getTime() + 24 * 60 * 60 * 1000));
				return InnerUtils.mapObject("src", s3.generatePresignedUrl(urlRequest).toString());
			} else {
				return InnerUtils.mapObject("src", "");
			}
		});
	}

	@Override
	public InputStream read(String location) throws Exception {
		if (ifRemoteFileExists(location)) {
			return s3task(s3 -> s3.getObject(buName, workingFolder(location)).getObjectContent());
		} else if (ifLocalFileExists(location)) {
			return new FileInputStream(localFile(location));
		} else {
			throw new Exception("File[location=" + location + "] dose not exist.");
		}
	}

	@Override
	public String write(InputStream in) throws Exception {
		String location = InnerUtils.uuid();
		OutputStream out = new FileOutputStream(localFile(location));
		InnerUtils.copyStreamAndClose(in, out);
		return location;
	}

	@Override
	public void delete(String location) throws Exception {
		s3task(s3 -> {
			s3.deleteObject(buName, workingFolder(location));
			return null;
		});
	}

	@Override
	public long sizeOf(String location) throws Exception {
		return s3task(s3 -> {
			if (ifRemoteFileExists(location)) {
				return s3.getObject(buName, workingFolder(location)).getObjectMetadata().getContentLength();
			} else {
				return localFile(location).length();
			}
		});
	}

	private boolean ifRemoteFileExists(String location) throws Exception {
		return s3task(s3 -> s3.doesObjectExist(buName, workingFolder(location)));
	}

	private boolean ifLocalFileExists(String location) {
		return localFile(location).exists();
	}

	private File localFile(String location) {
		return new File(localDirc().getPath() + FILE_SEPARATOR + "LOCAL_FILE_" + location);
	}

	private File localDirc() {
		String path = new File(System.getProperty("java.io.tmpdir")).getPath();
		File folder = new File(path + FILE_SEPARATOR + "AWS_LOCAL_TEMP_FILE_FOLDER");
		if (!folder.exists()) {
			folder.mkdirs();
		}
		return folder;
	}

	@Autowired
	private Environment env;

	private AmazonS3 newS3Client() {
		String accessKey = env.getProperty("aws.variables.s3.access_key");
		String secretId = env.getProperty("aws.variables.s3.secret_id");
		AWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretId);
		AWSCredentialsProvider awsCredentialsProvider = new AWSStaticCredentialsProvider(awsCredentials);
		ClientConfiguration config = new ClientConfiguration().withProtocol(com.amazonaws.Protocol.HTTP).withMaxConnections(300);
		return AmazonS3ClientBuilder.standard().withRegion(Regions.AP_SOUTHEAST_1).withCredentials(awsCredentialsProvider).withClientConfiguration(config).build();
	}

	private void buildS3IfNull() {
		if (s3client == null) {
			synchronized (this) {
				if (s3client == null) {
					s3client = newS3Client();
				}
			}
		}
	}

	private void destroyS3IfError(AmazonS3 currentS3) {
		// 1. Wasn't s3client built by other threads.
		if (currentS3 == s3client) {
			synchronized (currentS3) {
				// 2. Check again, wasn't s3client built by other threads.
				if (currentS3 == s3client) {
					s3client.shutdown();
					s3client = null;
				}
			}
		}
	}

	private AmazonS3 s3client;

	private <T> T s3task(S3TaskExecute<T> s3TaskExecute) throws Exception {
		return s3task(s3TaskExecute, 0);
	}

	private <T> T s3task(S3TaskExecute<T> s3TaskExecute, int errorCount) throws Exception {
		buildS3IfNull();
		AmazonS3 currentS3 = s3client;
		try {
			return s3TaskExecute.execute(currentS3);
		} catch (SdkBaseException | IllegalStateException e) {
			destroyS3IfError(currentS3);
			if (errorCount < 3) {
				return s3task(s3TaskExecute, ++errorCount);
			} else {
				throw e;
			}
		}
	}

	private static interface S3TaskExecute<T> {

		T execute(AmazonS3 s3client) throws Exception;
	}

	@Value("${aws.variables.s3.bucket}")
	private String buName;

	private String rootFolder() {
		return EnvUtils.getActive();
	}

	private String workingFolder() {
		return StringUtils.join(new String[] { rootFolder(), "working" }, FILE_SEPARATOR) + FILE_SEPARATOR;
	}

	private String workingFolder(String location) {
		return workingFolder() + location;
	}

	private String trashbnFolder() {
		return StringUtils.join(new String[] { rootFolder(), "trashbn" }, FILE_SEPARATOR) + FILE_SEPARATOR;
	}

	private String deletedFolder() {
		return StringUtils.join(new String[] { rootFolder(), "deleted" }, FILE_SEPARATOR) + FILE_SEPARATOR;
	}

	// -----------------Scan local file to upload or delete---------------------------------------------
	public void lazyUpladJob() {
		if (!localDirc().exists()) return;
		for (File temp : localDirc().listFiles()) {
			InnerUtils.tryCatch(() -> {
				String tempPath = temp.getPath().replaceAll("\\\\", FILE_SEPARATOR);
				String localPath = localFile("").getPath().replaceAll("\\\\", FILE_SEPARATOR);
				if (tempPath.indexOf(localPath) != 0) {
					//
				} else {
					String location = tempPath.substring(localPath.length());
					if (ifRemoteFileExists(location)) {
						temp.delete();
					} else {
						uploadLocalFile(location);
					}
				}
			});
		}
	}

	private void uploadLocalFile(String location) throws Exception {
		s3task(s3 -> {
			try (InputStream in = new FileInputStream(localFile(location))) {
				s3.putObject(buName, workingFolder(location), in, metaObject(in.available()));
			}
			return null;
		});
	}

	private ObjectMetadata metaObject(int available) {
		ObjectMetadata om = new ObjectMetadata();
		om.setContentLength(available);
		return om;
	}

	// -----------------Clear overdue file scheduled----------------------------------------------------
	public void cleanFileJob() {
		if (!deleting.compareAndSet(false, true)) {
			System.out.println("Deleting");
			return;
		} else {
			try {
				moveOriginalFiles();
				try {
					// 1. Get 10 S3 file objects from ${last_key_working}
					List<S3ObjectSummary> os = allSummaries(workingFolder(), last_key_working);
					// 2. Get the 10 file locations
					List<String> allFileLocationList = allFileLocationList(os, workingFolder());
					// 3. Get overdue locations
					List<String> overdueLocationList = overdueLocationList(os, workingFolder(), BIN_OVERDUE);
					// 4. Get "locations" that exist in database and overdue "locations"
					List<String> extFileLocationList = extFileLocationList(overdueLocationList);
					// 5. Get "locations" that needs to be moved to trash by excluding existing
					// locations from overdue locations
					List<String> outFileLocationList = outFileLocationList(overdueLocationList, extFileLocationList);
					// 6. Get left "locations" by excluding the "locations" that needs to be moved
					// to trash bin from all "locations"
					List<String> lftFileLocationList = lftFileLocationList(allFileLocationList, outFileLocationList);
					// 7. Assign the last location of left locations as the last_key_working for
					// next getting 10.
					last_key_working = findLastKey(workingFolder(), lftFileLocationList);
					// 8. Move the "locations" to trash folder from working folder.
					moveFileLocations(outFileLocationList, workingFolder(), trashbnFolder());
				} catch (Exception e) {
					InnerUtils.tryCatch(() -> {
						throw e;
					});
					last_key_working = null;
				}
				try {
					// 1. Get 10 S3 file objects from ${last_key_trashbn}
					List<S3ObjectSummary> os = allSummaries(trashbnFolder(), last_key_trashbn);
					// 2. Get the 10 file locations
					List<String> allFileLocationList = allFileLocationList(os, trashbnFolder());
					// 3. Get overdue locations
					List<String> overdueLocationList = overdueLocationList(os, trashbnFolder(), DEL_OVERDUE);
					// 4. Get left "locations" by excluding the "locations" that needs to be moved
					// to trash bin from all "locations"
					List<String> lftFileLocationList = lftFileLocationList(allFileLocationList, overdueLocationList);
					// 5. Move the "locations" to deleted folder from trash folder.
					moveFileLocations(overdueLocationList, trashbnFolder(), deletedFolder());
					// 6. Assign the last location of left locations as the last_key_trashbn for
					// next getting 10.
					last_key_trashbn = findLastKey(trashbnFolder(), lftFileLocationList);
				} catch (Exception e) {
					InnerUtils.tryCatch(() -> {
						throw e;
					});
					last_key_trashbn = null;
				}
			} catch (Exception e) {
				InnerUtils.tryCatch(() -> {
					throw e;
				});
			} finally {
				deleting.set(false);
			}
		}
	}

	private List<S3ObjectSummary> allSummaries(String workingFolder, String start) throws Exception {
		ListObjectsRequest req = new ListObjectsRequest(buName, workingFolder, start, null, 1000);
		ObjectListing listing = s3task(s3 -> s3.listObjects(req));
		List<S3ObjectSummary> os = listing.getObjectSummaries();
		List<S3ObjectSummary> list = os.stream().filter(o -> o.getKey().substring(workingFolder.length()).indexOf("/") < 0).collect(Collectors.toList());
		return list;
	}

	private List<String> overdueLocationList(List<S3ObjectSummary> os, String folder, long overdue) {
		return os.stream().filter(o -> new Date().getTime() - o.getLastModified().getTime() > overdue).map(o -> o.getKey().substring(folder.length())).collect(Collectors.toList());
	}

	private void moveFileLocations(List<String> fileLocations, String fromFolder, String toFolder) throws Exception {
		for (String fl : fileLocations) {
			s3task(s3 -> s3.copyObject(buName, fromFolder + fl, buName, toFolder + fl));
			if (s3task(s3 -> s3.doesObjectExist(buName, toFolder + fl))) {
				s3task(s3 -> {
					s3.deleteObject(buName, fromFolder + fl);
					return null;
				});
			}
		}
	}

	private List<String> outFileLocationList(List<String> allFileLocationList, List<String> extFileLocationList) throws Exception {
		List<String> outLocationList = new ArrayList<>(allFileLocationList);
		outLocationList.removeAll(extFileLocationList);
		return outLocationList;
	}

	private List<String> lftFileLocationList(List<String> allFileLocationList, List<String> outFileLocationList) throws Exception {
		List<String> lftFileLocationList = new ArrayList<>(allFileLocationList);
		lftFileLocationList.removeAll(outFileLocationList);
		return lftFileLocationList;
	}

	private List<String> extFileLocationList(List<String> allFileLocationSet) throws Exception {
		if (allFileLocationSet.size() == 0) {
			return new ArrayList<>();
		} else {
			GenericSrvProxy serv = BeanCtxUtils.getBeanByType(GenericSrvProxy.class);
			InnerCriteria cri = new InnerCriteria(new InnerCriterion("in", "location", allFileLocationSet.toArray()));
			cri.projections = new InnerProjection[] { new InnerProjection(null, "location") };
			List<?> list = serv.query(InnerFile.class, cri, null);
			Set<String> strSet = Stream.of(InnerUtils.getQueryListFirstFieldValues(list)).map(o -> (String) o).collect(Collectors.toSet());
			return new ArrayList<>(strSet);
		}
	}

	private List<String> allFileLocationList(List<S3ObjectSummary> os, String folder) {
		if (os.size() == 0) {
			return new ArrayList<>();
		} else {
			List<String> list = os.stream()// e
					.map(o -> o.getKey().substring(folder.length()))// e
					.collect(Collectors.toList());
			return new ArrayList<>(list);
		}
	}

	private String findLastKey(String folder, List<String> lftFileLocationList) {
		return lftFileLocationList.size() == 0 ? null : (folder + lftFileLocationList.get(lftFileLocationList.size() - 1));
	}

	private void moveOriginalFiles() throws Exception {
		String oriFolder = rootFolder() + "/";
		List<S3ObjectSummary> os = allSummaries(oriFolder, null);
		List<String> locations = allFileLocationList(os, oriFolder);
		moveFileLocations(locations, oriFolder, workingFolder());
	}

	private String last_key_working = null;

	private String last_key_trashbn = null;

	private final AtomicBoolean deleting = new AtomicBoolean(false);

	private final long BIN_OVERDUE = 1 * 24 * 60 * 60 * 1000l;

	private final long DEL_OVERDUE = 30 * 24 * 60 * 60 * 1000l;

	private final String FILE_SEPARATOR = "/";
}
